/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ca.inrs.ete.app.shopx;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.widget.ImageView;
import ca.inrs.ete.app.shopx.util.BitmapUtilities;

/**
 * This helper class Generate thumbnails from images and binds those with the provided ImageView.
 * A local cache of generated thumbnail images is maintained internally to improve performance.
 */
public class SHOPFilesViewTumbnailGenerator {
//	private static final String LOG_TAG = "SHOPLog.ThumbnailGen";

	/**
	 * Download the specified image from the Internet and binds it to the provided ImageView. The
	 * binding is immediate if the image is found in the cache and will be done asynchronously
	 * otherwise. A null bitmap will be associated to the ImageView if an error occurs.
	 *
	 * @param url The URL of the image.
	 * @param imageView The ImageView to bind the thumbnail image to.
	 */
	public void generateBitmap(Uri url, ImageView imageView, int width, int height) {
		resetPurgeTimer();
		Bitmap bitmap = getBitmapFromCache(url);

		if (bitmap == null) {
			forceGenerate(url, imageView, width, height);
		} else {
			cancelPotentialTask(url, imageView);
			imageView.setImageBitmap(bitmap);
		}
	}

	/**
	 * Same as generateBitmap but the image is always generated and the cache is not used.
	 * Kept private at the moment as its interest is not clear.
	 */
	private void forceGenerate(Uri url, ImageView imageView, int width, int height) {
		// State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys.
		if (url == null) {
			imageView.setImageDrawable(null);
			return;
		}

		if (cancelPotentialTask(url, imageView)) {
			BitmapWorkerTask task = new BitmapWorkerTask(imageView, width, height);
			GeneratedDrawable drawable = new GeneratedDrawable(task);
			imageView.setImageDrawable(drawable);
			task.execute(url);
		}
	}

	/**
	 * Returns true if the current task has been canceled or if there was no task in
	 * progress on this image view.
	 * Returns false if the task in progress deals with the same url. The task is not
	 * stopped in that case.
	 */
	private static boolean cancelPotentialTask(Uri url, ImageView imageView) {
		BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

		if (bitmapWorkerTask != null) {
			Uri bitmapUrl = bitmapWorkerTask.url;
			if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
				bitmapWorkerTask.cancel(true);
			} else {
				return false;
			}
		}
		return true;
	}

	/**
	 * @param imageView Any imageView
	 * @return Retrieve the currently active task (if any) associated with this imageView.
	 * null if there is no such task.
	 */
	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
		if (imageView != null) {
			Drawable drawable = imageView.getDrawable();
			if (drawable instanceof GeneratedDrawable) {
				GeneratedDrawable downloadedDrawable = (GeneratedDrawable)drawable;
				return downloadedDrawable.getBitmapDownloaderTask();
			}
		}
		return null;
	}

	/**
	 * The actual AsyncTask that will generate the thumbnail image.
	 */
	class BitmapWorkerTask extends AsyncTask<Uri, Void, Bitmap> {
		private final WeakReference<ImageView> imageViewReference;
		private Uri url;
		private int imgW;
		private int imgH;

		public BitmapWorkerTask(ImageView imageView, int width, int height) {
			imageViewReference = new WeakReference<ImageView>(imageView);
			imgW = width; 		// imageView.getWidth();
			imgH = height;		// imageView.getHeight();
		}

		/**
		 * Actual download method.
		 */
		@Override
		protected Bitmap doInBackground(Uri... params) {
			url = params[0];
			Bitmap roughBmp = BitmapUtilities.decodeResource(url, imgW, imgH);
	        Bitmap finalBmp = BitmapUtilities.createScaledBitmap(roughBmp, imgW, imgH);
	        roughBmp.recycle();
			return finalBmp;
		}

		/**
		 * Once the image is generated, associates it to the imageView
		 */
		@Override
		protected void onPostExecute(Bitmap bitmap) {
			if (isCancelled()) {
				bitmap = null;
			}

			addBitmapToCache(url, bitmap);

			if (imageViewReference != null) {
				ImageView imageView = imageViewReference.get();
				BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
				// Change bitmap only if this process is still associated with it
				if (this == bitmapWorkerTask) {
					imageView.setImageBitmap(bitmap);
				}
			}
		}
	}


	/**
	 * A fake Drawable that will be attached to the imageView while the download is in progress.
	 *
	 * <p>Contains a reference to the actual worker task, so that a task can be stopped
	 * if a new binding is required, and makes sure that only the last started process can
	 * bind its result, independently of the finish order.</p>
	 */
	static class GeneratedDrawable extends ColorDrawable {
		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

		public GeneratedDrawable(BitmapWorkerTask bitmapWorkerTask) {
			super(Color.BLACK);
			bitmapWorkerTaskReference =
					new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
		}

		public BitmapWorkerTask getBitmapDownloaderTask() {
			return bitmapWorkerTaskReference.get();
		}
	}

	/*
	 * Cache-related fields and methods.
	 * 
	 * We use a hard and a soft cache. A soft reference cache is too aggressively cleared by the
	 * Garbage Collector.
	 */

	private static final int HARD_CACHE_CAPACITY = 10;
	private static final int DELAY_BEFORE_PURGE = 10 * 1000; // in milliseconds

	// Hard cache, with a fixed maximum capacity and a life duration
	private final HashMap<Uri, Bitmap> sHardBitmapCache =
			new LinkedHashMap<Uri, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
		private static final long serialVersionUID = 739340308717246838L;

		@Override
		protected boolean removeEldestEntry(LinkedHashMap.Entry<Uri, Bitmap> eldest) {
			if (size() > HARD_CACHE_CAPACITY) {
				// Entries push-out of hard reference cache are transferred to soft reference cache
				sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
				return true;
			} else
				return false;
		}
	};

	// Soft cache for bitmaps kicked out of hard cache
	private final static ConcurrentHashMap<Uri, SoftReference<Bitmap>> sSoftBitmapCache =
			new ConcurrentHashMap<Uri, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);

	private final Handler purgeHandler = new Handler();

	private final Runnable purger = new Runnable() {
		public void run() {
			clearCache();
		}
	};

	/**
	 * Adds this bitmap to the cache.
	 * @param bitmap The newly generated bitmap.
	 */
	private void addBitmapToCache(Uri url, Bitmap bitmap) {
		if (bitmap != null) {
			synchronized (sHardBitmapCache) {
				sHardBitmapCache.put(url, bitmap);
			}
		}
	}

	/**
	 * @param url The URL of the image that will be retrieved from the cache.
	 * @return The cached bitmap or null if it was not found.
	 */
	private Bitmap getBitmapFromCache(Uri url) {
		// First try the hard reference cache
		synchronized (sHardBitmapCache) {
			final Bitmap bitmap = sHardBitmapCache.get(url);
			if (bitmap != null) {
				// Bitmap found in hard cache
				// Move element to first position, so that it is removed last
				sHardBitmapCache.remove(url);
				sHardBitmapCache.put(url, bitmap);
				return bitmap;
			}
		}

		// Then try the soft reference cache
		SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url);
		if (bitmapReference != null) {
			final Bitmap bitmap = bitmapReference.get();
			if (bitmap != null) {
				// Bitmap found in soft cache
				return bitmap;
			} else {
				// Soft reference has been Garbage Collected
				sSoftBitmapCache.remove(url);
			}
		}

		return null;
	}

	/**
	 * Clears the image cache used internally to improve performance. Note that for memory
	 * efficiency reasons, the cache will automatically be cleared after a certain inactivity delay.
	 */
	public void clearCache() {
		sHardBitmapCache.clear();
		sSoftBitmapCache.clear();
	}

	/**
	 * Allow a new delay before the automatic cache clear is done.
	 */
	private void resetPurgeTimer() {
		purgeHandler.removeCallbacks(purger);
		purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
	}
}
